#include "threads/synch.h"
#include "threads/thread.h"
#include "threads/producer-consumer.h"
#include "lib/stdio.h"

/* the bounded buffer which is filled with value extracted from TEST_STRING
 * for being accessed by producer and consumer */
static char bounded_buffer[BUFFER_SIZE];

// pointer to string which shall be accessed by the producers
static const char *src_string = TEXT;

// current producer(s) position in bounded_buffer
static int producer_ptr = 0;

// current consumer(s) position in bounded_buffer
static int consumer_ptr = 0;

/* current buffer size:
 * it is incremented after a producer has put a new element to buffer and
 * decremented after an item has been consumed by a consumer */
static int current_buffer_size = 0;

/* lock which is used by consumers as well as by producers to protect their critical
 * sections and condition variables */
static struct lock lock;

/* Condition on which producers wait while the buffer is full.
 * It is released by a consumer after its item consumption. */
static struct condition cond_buffer_not_full;

/* Condition on which consumers wait when the buffer is empty.
 * It is released by a producer after adding an element to the buffer */
static struct condition cond_buffer_not_empty;

/*
 * This function represents the producer implementation. It expects the aux
 * parameter for satisfying thread_func which is needed to create a thread
 * with it.*/
static void produce(void *aux)
{
	while(1)
	{
		// read the string which shall be written to bounded_buffer
		unsigned int i;
		for(i = 0; i < strlen(src_string); i++)
		{
			// src_string is read-only and can therefore be read without synchronization
			const char c = src_string[i];

			// start to protect critical section
			lock_acquire(&lock);
				/* Checked with a while-loop as it could be that
				 * when the thread leaves the cond_wait-statement,
				 * then the current_buffer_size can be again 0
				 * --> Race Condition */
				while(current_buffer_size == BUFFER_SIZE)
				{
					// wait until buffer is not full
					cond_wait(&cond_buffer_not_full, &lock);
				}

				// put read character into the allocated buffer
				bounded_buffer[producer_ptr] = c;
				// determine next position to write
				producer_ptr = (producer_ptr + 1) % BUFFER_SIZE;
				// increase number of items in buffer
				current_buffer_size++;
				// signal consumers that buffer is not empty anymore
				cond_signal(&cond_buffer_not_empty, &lock);
			// end of critical section
			lock_release(&lock);
		}
	}
}

/*
 * This function represents the consumer implementation. It expects the aux
 * parameter for satisfying thread_func which is needed to create a thread
 * with it.
 */
static void consume(void *aux)
{
	unsigned int i;
	while(1)
	{
		// start to protect critical section
		lock_acquire(&lock);
			/* Checked with a while-loop as it could be that
			 * when the thread leaves the cond_wait-statement,
			 * then the current_buffer_size can be again 0
			 * --> Race Condition */
			while(current_buffer_size == 0)
			{
				// wait until buffer is not empty
				cond_wait(&cond_buffer_not_empty, &lock);
			}

			// consume item from buffer
			const char c = bounded_buffer[consumer_ptr];
			// free position of item that was consumed
			bounded_buffer[consumer_ptr] = 0;
			// determine next customer read position
			consumer_ptr = (consumer_ptr + 1) % BUFFER_SIZE;
			// decrease number of items in buffer
			current_buffer_size--;
			// signal producers that buffer is not full anymore
			cond_signal(&cond_buffer_not_full, &lock);
			// print consumed item (IO operation is placed here to ensure right output order)
			printf("%c", c);

		// end of critical section
		lock_release(&lock);
	}
}

/**
 * Starts the producer consumer implementation. This function is linked to the test file.
 *
 * param num_prod_threads: number of producer threads
 * param num_cons_threads: number of consumer threads
 */
void start_producer_consumer(unsigned int num_prod_threads, unsigned int num_cons_threads)
{
	// initialize conditions and lock
	cond_init(&cond_buffer_not_full);
	cond_init(&cond_buffer_not_empty);
	lock_init(&lock);

	// create producer threads
	unsigned int i;
	for(i = 0; i < num_prod_threads; i++)
	{
		thread_create ("Producer Thread ", 0, produce, 0);
	}

	// create consumer threads
	for(i = 0; i < num_cons_threads; i++)
	{
		thread_create ("Consumer Thread", 0, consume, 0);
	}
}
